← Back to portfolio Case study · David Silverlight
Sherlock Cadence

Time & Expense, sculpted in soft UI.

A React Native app for the consultants a staffing agency places into client engagements: developers and designers billing hours across several projects a day, where every hour is both an invoice line and a paycheck line. One-tap capture, receipt scanning, and approval before invoicing, wrapped in a tactile neumorphic system where every control reads as physically pressable.

Punch IN / OUT Client & project codes Weekly timesheet Receipt OCR Manager approvals Offline-first sync
12Screens
6Redux slices
2Transports
1Tonal surface
2:185G
On the clockSince 6:02 AM
ELAPSED
8:1642
▲ 1.75 OT
Acme Fintech · Payments API
Break
Switch
Note
Slide to punch OUT
Active shift · dark soft-UIDebossed dial, raised action tiles
01 / The brief

Every hour is an invoice line and a paycheck line

The people using this app are the consultants a staffing agency places into client engagements: developers, designers and marketers, mostly at a desk, frequently remote, and often moving between two or three client projects in a single day. Their hours are not an administrative formality. Every hour is simultaneously a line on a client invoice and a line on a contractor's paycheck.

That dual role is what makes the record unforgiving. An hour booked to the wrong project is a billing dispute on one side and a payroll correction on the other, and both land on the agency. So the requirements are the ones Bullhorn Time & Expense set for the staffing industry: time tied to a specific job and cost code, corrections flagged for review rather than silently overwritten, expenses captured with a receipt at the point of spend, and an approval step that clears before anything reaches invoicing.

The real adversary here is not the clock, it is recall. A developer who rebuilds the week from memory on Friday afternoon rounds everything to the half hour, forgets the forty-minute production incident, and drops the context switch between clients entirely. Those missing hours are invisible: unbilled revenue for the agency and unpaid work for the contractor. Capture has to happen in the moment, in one tap, or it does not happen accurately at all.

And it can never fail quietly. A dropped VPN, hotel wifi, a train tunnel, a laptop that slept through a submit: none of those are allowed to cost someone an hour. A punch is recorded locally first and reconciled with the server later, so the app is never the reason time went unrecorded.

02 / My role

Design and build, end to end

I owned the product surface and the client architecture, from the design system through to the offline sync layer.

  • Designed the neumorphic system and all 12 screens
  • Built the React Native app in strict TypeScript
  • Split reads and writes across GraphQL and REST
  • Wrote the offline punch queue and reconciliation
  • Stood up the ASP.NET Core backend it talks to
03 / The design system

One surface, two light sources, zero borders

Neumorphism has a single rule: the entire interface is one tone, and depth comes from a paired shadow, light from the top left and dark from the bottom right. Nothing is outlined. A control is either extruded, meaning you can press it, or recessed, meaning it holds content or is already pressed. Every specimen below is live CSS, not an image.

RaisedActionable, sits proud
RecessedHolds or already pressed
Sign in
PrimaryThe one committed action
On the clock 1.75 OT
Status pillsRecessed, never shouty
ToggleOff recessed, on filled
8:16
DialRaised ring, debossed well
Surface#E5EAF1
Light source#FFFFFF
Dark source#A9B6CC
Ink#3B4759
Brand#5B6CDB
Confirm#2FB07A
Attention#E0A03C
Dark surface#2C313D
04 / The flow

Four acts, twelve screens

Sign in and know your status. Punch in against a client and project. Work the session. Submit, expense, and get approved. The dark variant is reserved for the two screens that stay open longest, sign-in and the running timer, where a dimmer surface is easier on the eyes across an early start or a late release.

9:415G
Good morning
Maria Torres
MT
Clocked out
0:00
You haven't started your shift
Punch IN
Acme Fintech · Payments API
This week
32.5 hrs
Expenses
$248 pend
Recent activity
See all
IN
Punched out
Acme Fintech · 8.0 hrs
Yest
$
Hardware, Best Buy #418
Pending approval
$129
Home
Time
Spend
Me
HomeThe punch control is the one raised element
9:415G
Punch IN
Billing to
Acme Fintech
Payments API · Sprint 34
Remote On site
Rate and PO verified
CLIENT / PROJECT
Acme Fintech · Payments API
WORK TYPE
DEV-204 · Feature work
Slide to punch IN
Punch IN · what you're billingRecessed engagement, raised code pickers
9:415G
Timesheet
Open
Jul 14 to Jul 20
This week
Total this week
39.75 hrs
36.0 reg
3.75 OT
M
Monday · Jul 14
6:00a to 2:30p
8.0
T
Tuesday · Jul 15
6:02a to 4:15p
9.5
W
Wed · Jul 16
In progress
8.3
Email card
Submit week
Weekly timesheetToday is recessed, finished days raised
9:415G
New expense
Cancel
Snap receipt, auto-scan
OCR ready
Amount
$129.00
Scanned from receipt
CategoryHardware / Peripherals
VendorBest Buy #418
Charge to clientAcme Fintech
Submit for approval
Add expenseRecessed camera well, OCR pre-fill
9:415G
Approvals
Acme Fintech · 6 waiting
6
Hours (4)Expenses (2)
JC
Jordan Chen
Payments API · Wed
1.75 OT
Submitted
9.75 hrs
6:00a to 4:15p
less 30 lunch
Reject
Approve
SP
Sam Park
Meridian Health · edited
Submitted
8.0 hrs
Missed punch
Approve all hours
ApprovalsOne queue for hours and spend
9:415G
Account
MT
Maria Torres
Senior Developer · Sherlock Talent · #10482
All punches synced
Last sync 2 min ago · 0 queued
Preferences
Face ID login
Location on on-site days
LanguageEnglish
Sign out
Account · syncQueue depth is never hidden
05 / Architecture

GraphQL to read, REST to write

Not a fashion choice, a shape choice. Screens want one round trip for exactly the data they render. Commands want verbs, status codes and multipart uploads. Both hit the same store, so the split is a transport decision rather than a data split.

READS · GraphQL + Apollo

One query per screen

The dashboard needs the user, week totals and recent activity together. One request returns that exact shape, with no over-fetching and no waterfall.

  • meprofile + role
  • currentTimesheetweek + days
  • jobs(search)assignment list
  • expenses(status)filtered spend
  • pendingApprovalssupervisor queue
WRITES · REST + axios

One verb per command

Punching, submitting a week, uploading a receipt and approving are discrete and side-effecting. They map cleanly onto HTTP, including multipart for receipt images.

  • POST /punchespunch + project
  • POST /timesheets/:id/submitsubmit week
  • PATCH /days/:dateflagged edit
  • POST /receipts/scanOCR pre-fill
  • POST /approvals/:id/decisionapprove

Six slices, three of them persisted

Redux Toolkit with redux-persist. Auth, punch and sync survive a cold start, because those three are what an offline punch depends on.

authJWT, profile, biometric gate
punchActive shift, elapsed, reconcile
syncOffline queue, last sync
timesheetWeek, days, submission
expenseDrafts, receipts, status
jobAssignments, cost codes
06 / The hard part

A punch that cannot be lost

A billable hour cannot depend on a request succeeding. Developers work over VPNs that drop mid-session, hotel and conference wifi, tethered phones and trains, and a failed POST must never be the reason an hour disappears. Every punch is recorded locally first, queued if the post fails, and reconciled with the server identity once the queue drains.

1RecordThe punch lands in the persisted punch slice immediately, with its timestamp, client and project.
2AttemptsubmitPunch posts to the REST API. On success the server identity comes straight back.
3EnqueueOn failure the payload is pushed into the persisted sync queue instead of being lost.
4FlushflushQueue drains on reconnect or app foreground, replaying punches and expenses in order.
5ReconcileServer IDs are merged back into the punch slice, so local and remote records converge.
07 / Reflection

What the surface cost, and what it bought

12Screens mapping 1:1 from design file to feature folder
0Borders in the entire interface
3Persisted slices guarding an offline punch
2Surface modes, light and dark soft-UI

The honest tradeoff: neumorphism is inherently low-contrast. It is a single tone doing all the work, which is exactly what makes it feel tactile and exactly what makes it a real accessibility risk, most of all for anyone with low vision and for the one-handed glance between meetings. I leaned on it because this app has very few controls per screen and one obvious action on each, so shape can carry the hierarchy that color usually would. But it does not get to ship alone. A high-contrast accessibility mode sits on the hardening list next to real JWT validation and background geofencing, and I would treat it as a release blocker rather than an enhancement.

08 / Under the hood

Stack

React NativeExpoTypeScript (strict)Redux Toolkitredux-persist Apollo ClientGraphQLaxiosRESTASP.NET Core 8 HotChocolateexpo-locationexpo-cameraexpo-local-authenticationJest